﻿using Flurl.Http;
using OkFlurl;
using System.IO.Compression;
using System.Text.Json;

namespace Loki.Client
{
    //https://grafana.com/docs/loki/latest/operations/multi-tenancy/
    //https://grafana.com/docs/loki/latest/reference/loki-http-api/
    public class LokiClient
    {
        #region Static Method

        private static string GetLogLevel(LokiLogLevel level)
        {
            var lv = level switch
            {
                LokiLogLevel.Debug => "debug",
                LokiLogLevel.Info => "info",
                LokiLogLevel.Warning => "warning",
                LokiLogLevel.Error => "error",
                LokiLogLevel.Fatal => "fatal",
                LokiLogLevel.Critical => "critical",
                _ => "unknown",
            };

            return lv;
        }

        private static List<LokiStream> CreateLogStreams(IEnumerable<LokiLogEntity> entitys)
        {
            if (entitys == null || !entitys.Any())
            {
                return [];
            }

            List<LokiStream> list = [];

            foreach (var entity in entitys)
            {
                if (string.IsNullOrEmpty(entity.App))
                {
                    throw new Exception("LokiLogEntity.App is null or empty.");
                }

                var id = $"{entity.LogLevel}-{entity.App}";
                if (entity.Labels != null && entity.Labels.Count > 0)
                {
                    id += "-" + string.Join(",", entity.Labels.Select(s => s.Key));
                }

                var stream = list.FirstOrDefault(f => f._id == id);
                if (stream == null)
                {
                    stream = new LokiStream
                    {
                        _id = id
                    };

                    stream.AddLabel("level", GetLogLevel(entity.LogLevel));
                    stream.AddLabel("app", entity.App);
                    if (entity.Labels != null && entity.Labels.Count > 0)
                    {
                        foreach (var lb in entity.Labels)
                        {
                            stream.AddLabel(lb.Key, lb.Value);
                        }
                    }
                    list.Add(stream);
                }

                if (entity.Time.HasValue)
                {
                    stream.AddValue(entity.Time.Value, entity.Message);
                }
                else
                {
                    stream.AddValue(DateTimeOffset.UtcNow, entity.Message);
                }
            }
            return list;
        }

        private static LokiStream CreateLogStream(string level, object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null)
        {
            if (string.IsNullOrEmpty(app))
            {
                throw new Exception("app is null or empty.");
            }

            var stream = new LokiStream();
            stream.AddLabel("app", app);
            stream.AddLabel("level", level);

            if (labels != null && labels.Count > 0)
            {
                foreach (var item in labels)
                {
                    stream.AddLabel(item.Key, item.Value);
                }
            }

            if (time.HasValue)
            {
                stream.AddValue(time.Value, msg);
            }
            else
            {
                stream.AddValue(DateTimeOffset.UtcNow, msg);
            }
            return stream;
        }

        #endregion

        #region MyRegion

        private readonly string _tenant;
        private readonly bool _gzip;
        private readonly CompressionLevel _compressLv;

        public FlurlApi Api { get; }

        public FlurlRequest CreateRequest(string path)
        {
            var req = Api.CreateRequest(path);
            if (!string.IsNullOrEmpty(_tenant))
            {
                req.Headers.Add("X-Scope-OrgID", _tenant);
            }
            return req;
        }

        #endregion

        public LokiClient(string url, string username = null, string password = null, string tenant = null, int? timeout = null, bool gzip = false, CompressionLevel compressLv = CompressionLevel.Optimal)
        {
            Api = new FlurlApi(url)
            {
                UserName = username,
                Password = password,
                Timeout = timeout
            };

            _tenant = tenant;
            _gzip = gzip;
            _compressLv = compressLv;
        }

        /// <summary>
        /// bulk insert LokiStream
        /// </summary>
        /// <param name="streams"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task InsertAsync(IEnumerable<LokiStream> streams, CancellationToken token = default)
        {
            var req = CreateRequest("/loki/api/v1/push");
            FlurlExt.InitContentType(req);
            var par = new { streams };

            if (!_gzip)
            {
                (await Api.PostAsync(req, par, token: token))?.Dispose();
                return;
            }

            req.WithHeader("Content-Encoding", "gzip");
            using var fcs = new FlurlStreamContent(async (stream) =>
            {
                using var gz = new GZipStream(stream, _compressLv, true);
                await JsonSerializer.SerializeAsync(gz, par, FlurlExt.JsonOptions, token);
                await gz.FlushAsync(token);
            });

            (await Api.PostAsync(req, fcs, token: token))?.Dispose();
        }

        /// <summary>
        /// insert LokiStream
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task InsertAsync(LokiStream stream, CancellationToken token = default)
        {
            await InsertAsync([stream], token);
        }

        /// <summary>
        /// bulk instert LokiLogEntity
        /// </summary>
        /// <param name="entitys"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task LogAsync(IEnumerable<LokiLogEntity> entitys, CancellationToken token = default)
        {
            var streams = CreateLogStreams(entitys);
            await InsertAsync(streams, token);
        }

        /// <summary>
        /// insert LokiLogEntity
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task LogAsync(LokiLogEntity entity, CancellationToken token = default)
        {
            await LogAsync([entity], token);
        }

        public async Task LogDebugAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("debug", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task LogInfoAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("info", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task LogWarningAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("warning", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task LogErrorAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("error", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task LogFatalAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("fatal", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task LogCriticalAsync(object msg, string app, Dictionary<string, string> labels = null, DateTimeOffset? time = null, CancellationToken token = default)
        {
            var stream = CreateLogStream("critical", msg, app, labels, time);
            await InsertAsync(stream, token);
        }

        public async Task<T> QueryLabels<T>(LokiQueryLabel query, CancellationToken token = default)
        {
            var req = CreateRequest("/loki/api/v1/labels");
            return await Api.GetJsonAsync<T>(req, query, token);
        }

        public async Task<T> QueryLabelValues<T>(string label, LokiQueryLabelValue query, CancellationToken token = default)
        {
            var req = CreateRequest($"/loki/api/v1/label/{label}/values");
            return await Api.GetJsonAsync<T>(req, query, token);
        }

        public async Task<T> QueryAsync<T>(LokiQueryRange query, CancellationToken token = default)
        {
            var req = CreateRequest("/loki/api/v1/query_range");
            return await Api.GetJsonAsync<T>(req, query, token);
        }

        public async Task<T> QueryVolumeRange<T>(LokiQueryVolumeRange query, CancellationToken token = default)
        {
            var req = CreateRequest("/loki/api/v1/index/volume_range");
            return await Api.GetJsonAsync<T>(req, query, token);
        }

        public async Task DeleteAsync(LokiQueryDelete query, CancellationToken token = default)
        {
            var req = CreateRequest("/loki/api/v1/delete");
            Api.InitGetPar(req, query);
            (await Api.PostAsync(req, null, token: token))?.Dispose();
        }

    }
}
